package Chess;

import java.awt.Color;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Iterator;
import java.util.ArrayList;
import Chess.Pieces.*;
import Chess.Pieces.Piece.PieceType;
import Chess.Moves.*;
import Chess.Moves.Move.MoveType;

/**
 *	A class handling the overall game operations.
 *	Coordinates the Board, the Moves, and the Pieces.
 *	
 *	@author		Jeff Chan
 */
public class Game {
	private Board board;
	private ArrayList<Move> moves;
	private Color currentPlayer;
	private Map<Color, ArrayList<Piece>> capturedPieces;
	
	private static char[] FILES = new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'};
	
	/**
	 *	Class constructor initilizing the game.
	 */
	public Game() {
		initializeGame();
	}
	
	/**
	 *	Method that sets the Board to starting positions.
	 *	Also used to reset the Game.
	 */
	public void initializeGame() {
		clearCapturedPieces();
		board = new Board();
		moves = new ArrayList<Move>();
		currentPlayer = Color.WHITE;
		putBlackPieces();
		putWhitePieces();
	}
	
	/**
	 *	Executes a Move from given the starting and
	 *	end Squares.
	 *
	 *	@param	startSquare		the Square of the playing Piece
	 *	@param	endSquare		the Square of the desination
	 *	@return					the Move executed
	 */
	public Move move(Square startSquare, Square endSquare) {
		Move move = InvalidMove.execute();
		
		
		// Must Piece to be moved must be of the player's color
		if(squareContainsPlayersPiece(startSquare)) {
			move = executeMove(startSquare, endSquare);
			
			if(!move.getMoveType().equals(MoveType.INVALID)) {
				// Stores the Move into history
				moves.add(move);
				changePlayer();
				
				if(isCheckMate(getCurrentPlayer())) {
					moves.add(CheckmateMove.execute());
				}
			}
		}
		
		return move;
	}
	
	/**
	 *	Attempts to execute a PromotionMove. Returns an instance of
	 *	InvalidMove of promotion can't be executed.
	 *
	 *	@param	startSquare	the Square of the playing Piece
	 *	@param	endSquare	the Square of the destination
	 *	@param	pieceType	the desired Piece after promotion
	 *	@return 			the resulting Move
	 */
	public Move promote(Square startSquare, Square endSquare, PieceType pieceType) {
		Move move = InvalidMove.execute();

		if(PromotionMove.canBeExecuted(board, startSquare, endSquare, pieceType)) {
			move = PromotionMove.execute(board, startSquare, endSquare, pieceType);
		}
		
		// If successfully executed...
		if(MoveType.PROMOTION.equals(move.getMoveType())) {
			
			// Does the result form a check situation?
			if(board.isInCheck(currentPlayer)) {
				// Yes! Undo this...
				move.undo(board);
				move = InvalidMove.execute();
			} else {
				// Nope.. If we captured a Piece, let's store it.
				PromotionMove promotionMove = (PromotionMove) move;
				if(promotionMove.getOpposingPiece() != null) {
					capturedPieces.get(promotionMove.getOpposingPiece().getColor()).add(promotionMove.getOpposingPiece());
				}
			}
		}
		
		return move;
	}
	
	/**
	 *	Executes a Move from given the starting and
	 *	end Squares. Does so by checking through each Move types.
	 *	Returns InvalidMove if the move results in a check situtation.
	 *
	 *	@param	startSquare		the Square of the playing Piece
	 *	@param	endSquare		the Square of the desination
	 *	@return					the Move executed
	 */
	private Move executeMove(Square startSquare, Square endSquare) {
		Move move = InvalidMove.execute();
		
		if(isPromotionMove(startSquare, endSquare)) {
			move = promote(startSquare, endSquare, PieceType.QUEEN);
		} else if(EnPassantMove.canBeExecuted(board, startSquare, endSquare, getLastMove())) {
			move = EnPassantMove.execute(board, startSquare, endSquare);
			EnPassantMove enPassantMove = (EnPassantMove) move;
			capturedPieces.get(enPassantMove.getOpposingPiece().getColor()).add(enPassantMove.getOpposingPiece());
		} else if(CastlingMove.canBeExecuted(board, startSquare, endSquare)) {
			move = CastlingMove.execute(board, startSquare, endSquare);
		} else if(NormalMove.canBeExecuted(board, startSquare, endSquare)) {
			move = NormalMove.execute(board, startSquare, endSquare);
		} else if(CaptureMove.canBeExecuted(board, startSquare, endSquare)) {
			move = CaptureMove.execute(board, startSquare, endSquare);
			CaptureMove captureMove = (CaptureMove) move;
			capturedPieces.get(captureMove.getOpposingPiece().getColor()).add(captureMove.getOpposingPiece());
		}
		
		// Does the Move result in a check?
		if(board.isInCheck(currentPlayer)) {
			// Oops, it does. Let's undo!
			move.undo(board);
			removeCapturedPiece(move);
			move = InvalidMove.execute();
		}

		return move;
	}
	
	/**
	 *	Return true if a PromotionMove can be executed.
	 *	Set to false if not.
	 *	
	 *	@param	startSquare		the starting Square of this Move
	 *	@param	endSquare		the ending Square of this Move
	 *	@return					true if PromotionMove can be executed
	 */
	private boolean isPromotionMove(Square startSquare, Square endSquare) {
		boolean isPromotionMove = false;
		
		Piece piece = board.getPiece(startSquare);
		if(piece != null && PieceType.PAWN.equals(piece.getPieceType()) && endSquare.isInLastRow(piece.getColor())) {
			isPromotionMove = true;
		}
		
		return isPromotionMove;
	}
	
	/**
	 *	Removes the captured piece from the capturedPieces history
	 *	in an effort to undo the given Move.
	 *
	 *	@param	move		the Move to undo
	 */
	private void removeCapturedPiece(Move move) {
		Piece capturedPiece = null;
		if(MoveType.CAPTURE.equals(move.getMoveType())) {
			capturedPiece = ((CaptureMove)move).getOpposingPiece();
		} else if(MoveType.EN_PASSANT.equals(move.getMoveType())) {
			capturedPiece = ((EnPassantMove)move).getOpposingPiece();
		} else if(MoveType.PROMOTION.equals(move.getMoveType())) {
			capturedPiece = ((PromotionMove)move).getOpposingPiece();
		}
		
		if(capturedPiece != null) {
			capturedPieces.get(capturedPiece.getColor()).remove(capturedPiece);
		}
	}
	
	/**
	 *	Returns true if this Square contains the current player's Piece.
	 *	Set to false if not.
	 *
	 *	@param	startSquare		the Square to test
	 *	@return					true if this Square contains the current player's Piece
	 */
	private boolean squareContainsPlayersPiece(Square startSquare) {
		if(board.getPiece(startSquare) == null || !board.getPiece(startSquare).getColor().equals(currentPlayer)) {
			return false;
		}
		return true;
	}
	
	/**
	 *	Places the WHITE Pieces into their respective starting Squares.
	 */
    private void putWhitePieces() {
		board.putPiece(new Square("a1"), new Rook(Color.WHITE));
		board.putPiece(new Square("b1"), new Knight(Color.WHITE));
		board.putPiece(new Square("c1"), new Bishop(Color.WHITE));
		board.putPiece(new Square("d1"), new Queen(Color.WHITE));
		board.putPiece(new Square("e1"), new King(Color.WHITE));
		board.putPiece(new Square("f1"), new Bishop(Color.WHITE));
		board.putPiece(new Square("g1"), new Knight(Color.WHITE));
		board.putPiece(new Square("h1"), new Rook(Color.WHITE));
		for (char row : FILES) {
		        board.putPiece(new Square(row, '2'), new Pawn(Color.WHITE));
		}              
    }
   
   /**
	 *	Places the BLACK Pieces into their respective starting Squares.
	 */
	private void putBlackPieces() {
		board.putPiece(new Square("a8"), new Rook(Color.BLACK));
		board.putPiece(new Square("b8"), new Knight(Color.BLACK));
		board.putPiece(new Square("c8"), new Bishop(Color.BLACK));
		board.putPiece(new Square("d8"), new Queen(Color.BLACK));
		board.putPiece(new Square("e8"), new King(Color.BLACK));
		board.putPiece(new Square("f8"), new Bishop(Color.BLACK));
		board.putPiece(new Square("g8"), new Knight(Color.BLACK));
		board.putPiece(new Square("h8"), new Rook(Color.BLACK));
		for (char row : FILES) {
			board.putPiece(new Square(row, '7'), new Pawn(Color.BLACK));
		}
	}
	
	/**
	 *	Clears the capturedPieces history. Used to reset.
	 */
    private void clearCapturedPieces() {
		capturedPieces = new HashMap<Color, ArrayList<Piece>>();
		capturedPieces.put(Color.WHITE, new ArrayList<Piece>());
		capturedPieces.put(Color.BLACK, new ArrayList<Piece>());
    }
	
	/**
	 *	Switches the player turn. BLACK to WHITE or vice versa.
	 */
	public void changePlayer() {
		if(currentPlayer.equals(Color.WHITE))
			currentPlayer = Color.BLACK;
		else
			currentPlayer = Color.WHITE;
	}
	
	/**
	 *	Returns the Board the game is played on.
	 *	
	 *	@return		the Board played on
	 */
	public Board getBoard() {
		return board;
	}
	
	/**
	 *	Returns the Color of the current player
	 *	
	 *	@return		the Color of the current player
	 */
	public Color getCurrentPlayer() {
		return currentPlayer;
	}
	
	/**
	 *	Returns the String representation Color of the current player
	 *	
	 *	@return		the String representation Color of the current player
	 */
	public String getCurrentPlayerString() {
		if(currentPlayer.equals(Color.WHITE))
			return "White";
		else
			return "Black";
	}
	
	/**
	 *	Returns the ArrayList of historical Moves
	 *	
	 *	@return		the ArrayList of Moves
	 */
	public ArrayList<Move> getMoves() {
		return moves;
	}
	
	/**
	 *	Undos the previous move.
	 */
	public void undo() {
		if(getLastMove() != null)
			getLastMove().undo(board);
	}
	
	/**
	 *	Gets the captured Pieces for a given Color.
	 *
	 *	@param	color	the desired color	
	 *	@return 		an ArrayList of the captured Pieces
	 */
	public ArrayList<Piece> getCapturedPieces(Color color) {
		return capturedPieces.get(color);
	}
	
	/**
	 *	Returns the last executed move.
	 *	
	 *	@return		the last executed move
	 */
	public Move getLastMove() {
		if(moves.size() > 0)
			return moves.get(moves.size() -1);
		else
			return null;
	}
	
	/**
	 *	Returns true if the given Color is at a checkmate situation.
	 *	Set to false if not.
	 *	
	 *	@param	color	the color to test
	 *	@return			true if checkmate for the color
	 */
	public boolean isCheckMate(Color color) {
		HashMap<Square, Piece> grid = board.getGrid();
		Set<Square> squares = grid.keySet();
		for (Iterator<Square> squareIter = squares.iterator(); squareIter.hasNext();) {
			try {
				Square square = squareIter.next();
				for (char i='1'; i<='8'; i--) { for(char row : new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}) {
					if(canMoveTo(square, new Square(row, i))) {
						return false;
					}
				}}
			} catch(Exception i) {
				return true;
			}
		}

		return true;
	}
	
	/**
	 *	Returns true if the Piece in the startSquare can move to
	 *	the endSquare. Set to false if it cannot.
	 *	
	 *	@param	startSquare		the starting Square for this test
	 *	@param	endSquare		the ending Square for this test
	 *	@return					true if move is possible from one square to the other
	 */
	public boolean canMoveTo(Square startSquare, Square endSquare) {
		if(!squareContainsPlayersPiece(startSquare))
			return false;
		
		Move move = executeMove(startSquare, endSquare);
		if(move.getMoveType() != MoveType.INVALID) {
			move.undo(board);
			removeCapturedPiece(move);
			return true;
		}
		
		return false;
	}
}